package org.xdoclet.plugin.hibernate;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.generama.FreeMarkerTemplateEngine;
import org.generama.QDoxCapableMetadataProvider;
import org.generama.WriterMapper;
import org.generama.defaults.QDoxPlugin;
import org.xdoclet.plugin.hibernate.qtags.HibernateFilterParamTag;
import org.xdoclet.plugin.hibernate.qtags.HibernateTypedefParamTag;
import org.xdoclet.predicate.HasTag;

import com.thoughtworks.qdox.model.AbstractJavaEntity;
import com.thoughtworks.qdox.model.BeanProperty;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMethod;

import freemarker.ext.beans.BeansWrapper;

public abstract class AbstractHibernateFreemarkerPlugin extends QDoxPlugin {
	static final String TAG_PREFIX = "hibernate.";
	static final Log log = LogFactory.getLog(OracleCommentsPlugin.class);
	static final Map tagDispatch = new HashMap();

	// list holding tag names defining property existence
	static final List PROPERTY_TAGS = new ArrayList();

	// list holding tag names defining property existence
	static final List ALLOWED_IN_PROPERTIES_TAGS = new ArrayList();

	// list holding tag names allowed in join tag
	static final List ALLOWED_IN_JOIN_TAGS = new ArrayList();

	// list holding tag names causing stop of recursive search for properties
	static final List HIERARCHY_STOP_TAGS = new ArrayList();

	// list holding tag names defining ID existence
	static final List ID_TAGS = new ArrayList();

	// list holding tag names defining ID existence
	static final List VERSION_TAGS = new ArrayList();

	static {
		PROPERTY_TAGS.add("hibernate.custome");
		PROPERTY_TAGS.add("hibernate.property");
		PROPERTY_TAGS.add("hibernate.many-to-one");
		PROPERTY_TAGS.add("hibernate.one-to-one");
		PROPERTY_TAGS.add("hibernate.component");
		PROPERTY_TAGS.add("hibernate.dynamic-component");
		PROPERTY_TAGS.add("hibernate.any");
		PROPERTY_TAGS.add("hibernate.map");
		PROPERTY_TAGS.add("hibernate.set");
		PROPERTY_TAGS.add("hibernate.list");
		PROPERTY_TAGS.add("hibernate.bag");
		PROPERTY_TAGS.add("hibernate.idbag");
		PROPERTY_TAGS.add("hibernate.array");
		PROPERTY_TAGS.add("hibernate.primitive-array");
		PROPERTY_TAGS.add("hibernate.key-property");
		PROPERTY_TAGS.add("hibernate.key-many-to-one");
		PROPERTY_TAGS.add("hibernate.parent");
		HIERARCHY_STOP_TAGS.add("hibernate.class");
		HIERARCHY_STOP_TAGS.add("hibernate.subclass");
		HIERARCHY_STOP_TAGS.add("hibernate.joined-subclass");
		HIERARCHY_STOP_TAGS.add("hibernate.union-subclass");
		ID_TAGS.add("hibernate.id");
		ID_TAGS.add("hibernate.composite-id");
		VERSION_TAGS.add("hibernate.version");
		VERSION_TAGS.add("hibernate.timestamp");
		ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.property");
		ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.many-to-one");
		ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.component");
		ALLOWED_IN_PROPERTIES_TAGS.add("hibernate.dynamic-component");
		ALLOWED_IN_JOIN_TAGS.add("hibernate.property");
		ALLOWED_IN_JOIN_TAGS.add("hibernate.many-to-one");
		ALLOWED_IN_JOIN_TAGS.add("hibernate.component");
		ALLOWED_IN_JOIN_TAGS.add("hibernate.dynamic-component");
	}

	private boolean force = false;
	private Stack componentsPrefixies = new Stack();

	private String version = HibernateVersion.HIBERNATE_2_0;

	private Collection allClasses;

	// public
	// OracleCommentsPlugin(JellyTemplateEngine/*FreeMarkerTemplateEngine*/
	// jellyTemplateEngine,
	public AbstractHibernateFreemarkerPlugin(FreeMarkerTemplateEngine freemarkerTemplateEngine,
			QDoxCapableMetadataProvider metadataProvider,
			WriterMapper writerMapper) {
		super(freemarkerTemplateEngine, metadataProvider, writerMapper);
	}

	public String getDTDVersion() {
		return version;
	}

	/**
	 * Specify version of Hibernate.
	 * 
	 * @generama.property required="false" default="2.0"
	 *                    allowed-values="2.0, 3.0"
	 */
	public void setVersion(String version) {
		String[] values = HibernateVersion.getValues();

		for (int i = 0; i < values.length; i++) {
			if (values[i].equals(version)) {
				this.version = version;
				return;
			}
		}

		throw new IllegalArgumentException("Incorrect hibernate version : "
				+ version);
	}

	public Collection getMappedClasses() {
		if (allClasses == null) {
			allClasses = metadataProvider.getMetadata();
		}

		return CollectionUtils.select(allClasses, new HasTag("hibernate.class",
				null, null, false));
	}

	/**
	 * Get all candidates for properties tag
	 */
	public List getAlternateKeyProperties(JavaClass clazz,
			final String propertiesName) {
		if (propertiesName == null) {
			return null;
		}

		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, null, true,
				ALLOWED_IN_PROPERTIES_TAGS, retval);
		CollectionUtils.filter(retval, new Predicate() {
			public boolean evaluate(Object object) {
				HibernateProperty prop = (HibernateProperty) object;
				DocletTag tag = getTagByNameList(prop.getEntity(),
						ALLOWED_IN_PROPERTIES_TAGS);

				return tag != null
						&& propertiesName.equalsIgnoreCase(tag
								.getNamedParameter("properties-name"));

			}
		});
		return retval;
	}

	/**
	 * Get all candidates for join tag
	 */
	public List getJoinProperties(JavaClass clazz, final String joinName) {
		if (joinName == null) {
			return null;
		}

		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, null, true, ALLOWED_IN_JOIN_TAGS,
				retval);
		CollectionUtils.filter(retval, new Predicate() {
			public boolean evaluate(Object object) {
				HibernateProperty prop = (HibernateProperty) object;
				DocletTag tag = getTagByNameList(prop.getEntity(),
						ALLOWED_IN_JOIN_TAGS);

				return tag != null
						&& joinName.equalsIgnoreCase(tag
								.getNamedParameter("join-name"));

			}
		});
		return retval;
	}

	/**
	 * Get at least one doclet tag from javaEntity
	 * 
	 * @param entity
	 *            entity which have doclet tags
	 * @param tagNames
	 *            list of possible tag names
	 * @return found doclet tag or null if there is no such tag
	 */
	public DocletTag getTagByNameList(AbstractJavaEntity entity, List tagNames) {
		for (int i = 0; i < tagNames.size(); i++) {
			String tag = (String) tagNames.get(i);
			DocletTag docletTag = entity.getTagByName(tag);

			if (docletTag != null) {
				return docletTag;
			}
		}

		return null;
	}

	/**
	 * provide list of properties candidating for class id
	 * 
	 * @param clazz
	 * @return
	 */
	public List getClassId(JavaClass clazz) {
		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, null, true, ID_TAGS, retval);
		return retval;
	}

	/**
	 * provide list of hibernate properties for class. it could be getter, as
	 * well as field ( for direct property access )
	 */
	public List getClassProperties(JavaClass clazz) {
		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, null, true, PROPERTY_TAGS, retval);
		return retval;
	}

	/**
	 * Get list of properties for component class. Real class is calculated in
	 * following sequence. First tries to find class by its name if classname
	 * parameter is null or class not found
	 * 
	 * @param componentClassName
	 * @param componentPropertyClass
	 * @return
	 */
	public List getComponentProperties(String componentClassName,
			JavaClass componentPropertyClass) {
		JavaClass componentClass;

		if (componentClassName != null) {
			componentClass = getMetadata(componentClassName);
		} else {
			componentClass = componentPropertyClass;
		}

		return getClassProperties(componentClass);
	}

	/**
	 * Returns first argument wich is not empty or last argument if all are
	 * empty
	 * 
	 * @param value1
	 *            first argument
	 * @param value2
	 *            second argument
	 * @return
	 */
	public String getFirstNonEmptyValue(String value1, String value2) {
		return value1 != null && value1.trim().length() > 0 ? value1 : value2;
	}

	public void setForce(boolean force) {
		this.force = force;
	}

	/**
	 * Return the instance of JavaClass by it's name
	 */
	public JavaClass getMetadata(final String className) {
		if (className == null) {
			throw new NullPointerException("Classname can't be null");
		}

		JavaClass javaClass = (JavaClass) CollectionUtils.find(getMetadata(),
				new Predicate() {
					public boolean evaluate(Object o) {
						JavaClass cl = (JavaClass) o;
						return cl.getFullyQualifiedName().equals(className);
					}
				});

		if (javaClass == null) {
			log.error("Sourcecode for class '" + className
					+ "' not found by metadata povider");
			throw new IllegalArgumentException("Class metadata for "
					+ className + " not found");
		}

		return javaClass;
	}

	/**
	 * derive property name from metadata. strip / decapitalize
	 */
	public String getPropertyName(AbstractJavaEntity metadata) {
		if (metadata instanceof JavaMethod) {
			return ((JavaMethod) metadata).getPropertyName();
		} else if (metadata instanceof JavaField) {
			return metadata.getName();
		}

		// maybe throw exception?
		return null;
	}

	/**
	 * provide list of valid property tags
	 */
	public List getPropertyTagList() {
		return PROPERTY_TAGS;
	}

	/**
	 * provide list of valid property tags that can be used inside <properties>
	 * tag
	 */
	public List getTagListAllowedInProperties() {
		return ALLOWED_IN_PROPERTIES_TAGS;
	}

	/**
	 * provide list of valid property tags that can be used inside <join> tag
	 */
	public List getTagListAllowedInJoin() {
		return ALLOWED_IN_JOIN_TAGS;
	}

	/**
	 * provide list of hibernate properties for subclass. it could be getter, as
	 * well as field ( for direct property access ). we stop at hibernate.class
	 */
	public List getSubclassProperties(JavaClass clazz) {
		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, HIERARCHY_STOP_TAGS, true,
				PROPERTY_TAGS, retval);
		return retval;
	}

	/**
	 * provide list of subclasses for given class
	 */
	public List getSubclasses(JavaClass clazz, String tagName) {
		List result = new ArrayList();
		getSubclassesWithTagRecursive(result, clazz, tagName);
		Collections.sort(result);
		return result;
	}

	/**
	 * provide combined list of all the tags with given tag names
	 */
	public List getTags(AbstractJavaEntity metadata, Collection tagNames) {
		List al = new ArrayList();

		for (Iterator iter = tagNames.iterator(); iter.hasNext();) {
			al.addAll(Arrays.asList(metadata.getTagsByName((String) iter.next())));
		}

		return al;
	}

	/**
	 * provide list of tags from pipe separated string
	 */
	public List getTags(AbstractJavaEntity metadata, String tagNames) {
		return getTags(metadata, Arrays.asList(tagNames.split("\\|")));
	}

	public Collection getTypedefParams(JavaClass clazz, final String typedefName) {
		if (typedefName == null) {
			return null;
		}

		Collection typedefTags = new ArrayList();
		typedefTags.addAll(Arrays.asList(clazz
				.getTagsByName("hibernate.typedef-param")));
		CollectionUtils.filter(typedefTags, new Predicate() {
			public boolean evaluate(Object object) {
				HibernateTypedefParamTag tag = (HibernateTypedefParamTag) object;
				return typedefName.equals(tag.getTypedefName());
			}
		});
		return typedefTags;
	}

	public Collection getFilterdefParams(JavaClass clazz,
			final String filterdefName) {
		if (filterdefName == null) {
			return null;
		}
		Collection filterdefTags = new ArrayList();
		filterdefTags.addAll(Arrays.asList(clazz.getTagsByName(
				"hibernate.filter-param", true)));
		CollectionUtils.filter(filterdefTags, new Predicate() {
			public boolean evaluate(Object object) {
				HibernateFilterParamTag tag = (HibernateFilterParamTag) object;
				return filterdefName.equals(tag.getFilterdefName());
			}
		});
		return filterdefTags;
	}

	/**
	 * provide list of properties candidating for version or timestamp
	 */
	public List getVersionOrTimestamp(JavaClass clazz) {
		List retval = new ArrayList();
		accumulatePropertiesRecursive(clazz, null, true, VERSION_TAGS, retval);
		return retval;
	}

	/**
	 * dispatch qtag to correct jelly script. cache results statically
	 */
	public String dispatchTag(String tagName) {
		String retval = (String) tagDispatch.get(tagName);

		if (retval == null) {
			// rig up correct script name from tag name
			// first, kill hibernate
			if (!tagName.startsWith(TAG_PREFIX)) {
				return null;
			}

			// split on dash and capitalize first chars
			String[] nameParts = tagName.substring(TAG_PREFIX.length()).split(
					"-");
			StringBuffer scriptName = new StringBuffer("/");

			for (int i = 0; i < nameParts.length; i++) {
				scriptName
						.append(Character.toUpperCase(nameParts[i].charAt(0)));
				scriptName.append(nameParts[i].substring(1));
			}

			scriptName.append(".jelly");
			retval = scriptName.toString();
			log.debug("Dispatch tag " + tagName + " to script " + retval);
			tagDispatch.put(tagName, retval);
		}

		return retval;
	}

	public boolean hasAtLeastOne(AbstractJavaEntity metadata, Collection tags) {
		return getTags(metadata, tags).size() >= 1;
	}

	public boolean hasAtMostOne(AbstractJavaEntity metadata, Collection tags) {
		return getTags(metadata, tags).size() <= 1;
	}

	public boolean hasOnlyOne(AbstractJavaEntity metadata, Collection tags) {
		return getTags(metadata, tags).size() == 1;
	}

	/**
	 * whether we sould generate given class. we generate if class contains
	 * hibernate.class tag on it. class could be as well abstract, because real
	 * stuff lives in polymorphic subclasses
	 */
	public boolean shouldGenerate(Object metadata) {
		JavaClass clazz = (JavaClass) metadata;

		// Check if mapping up-to-date then skip generation
		if (!force) {
			String packagePath = getDestinationPackage(metadata).replace('.',
					'/');
			File dir = new File(getDestdirFile(), packagePath);
			String filename = getDestinationFilename(metadata);
			File destFile = new File(dir, filename);
			File sourceFile = new File(clazz.getSource().getURL().getFile());

			if (destFile.exists()
					&& sourceFile.lastModified() < destFile.lastModified()) {
				return false;
			}
		}

		// we refuse to generate if object is in restricted folder
		if (!super.shouldGenerate(metadata)) {
			return false;
		}
		boolean generate = clazz.getTagByName("hibernate.class") != null;

		if (generate) {
			System.out.println("  * Generate mapping for '" + clazz.getName()
					+ "' entity");
		}

		return generate;
	}

	/**
	 * gather hibernate propertis from given class into list
	 */
	private void accumulateProperties(JavaClass clazz,
			Collection requieredTags, List accumulate) {
		// walk through property getters
		BeanProperty[] beanProperties = clazz.getBeanProperties();
		HibernateProperty property;

		for (int i = 0; i < beanProperties.length; i++) {
			// property is ours, if we have at least one of designated property
			// tags and there is accessor

			if ((beanProperties[i] != null)
					&& (beanProperties[i].getAccessor() != null)
					&& !getTags(beanProperties[i].getAccessor(), requieredTags)
							.isEmpty()) {
				property = new HibernateProperty();
				property.setName(beanProperties[i].getName());
				property.setEntity(beanProperties[i].getAccessor());
				// we do not specify access setting unless there is explicit
				// setting
				if (((DocletTag) getTags(beanProperties[i].getAccessor(),
						requieredTags).get(0)).getNamedParameter("access") != null) {
					property.setAccess(((DocletTag) getTags(
							beanProperties[i].getAccessor(), requieredTags)
							.get(0)).getNamedParameter("access"));
				}

				if (!accumulate.contains(property)) {
					accumulate.add(property);
				}
			}
		}

		JavaField[] fields = clazz.getFields();

		for (int i = 0; i < fields.length; i++) {
			if (!getTags(fields[i], requieredTags).isEmpty()) {
				property = new HibernateProperty();
				property.setName(fields[i].getName());
				property.setEntity(fields[i]);
				// if no property access is specified, we shall use field
				// else take whatever user says ( leave it to his discretion to
				// use wrong specification )
				if (((DocletTag) getTags(fields[i], requieredTags).get(0))
						.getNamedParameter("access") != null) {
					property.setAccess(((DocletTag) getTags(fields[i],
							requieredTags).get(0)).getNamedParameter("access"));
				} else {
					property.setAccess("field");
				}

				if (!accumulate.contains(property)) {
					accumulate.add(property);
				}
			}
		}
	}

	/**
	 * recursive property retrival stopping at stop tag
	 * 
	 * @param skipStopTags
	 *            needs more explanation here. We need to skip checking on stop
	 *            tags on the first method invocation (I mean when we didnt dive
	 *            into recursion cycle yet) because it have already
	 *            @hibernate.property tag (which is stop tag by itself)
	 */
	private void accumulatePropertiesRecursive(JavaClass clazz,
			Collection stopTags, boolean skipStopTags, Collection requiredTags,
			List accumulate) {
		// stop recursion?
		if (!skipStopTags && stopTags != null
				&& !getTags(clazz, stopTags).isEmpty()) {
			// yep.
			return;
		}

		accumulateProperties(clazz, requiredTags, accumulate);
		// Look at subclass
		JavaClass superclass = clazz.getSuperJavaClass();

		if (superclass != null) {
			accumulatePropertiesRecursive(superclass, stopTags, false,
					requiredTags, accumulate);
		}

		// Browse over all implemented interfaces
		JavaClass[] ifaces = clazz.getImplementedInterfaces();

		for (int i = 0; i < ifaces.length; i++) {
			accumulatePropertiesRecursive(ifaces[i], stopTags, false,
					requiredTags, accumulate);
		}
	}

	/**
	 * recursively retrieve list of derived classes with certain tag, cut search
	 * on found tag. also preveent double addition of subclass to the list ( due
	 * to interface inheritance )
	 */
	private void getSubclassesWithTagRecursive(List found, JavaClass clazz,
			String tagName) {
		JavaClass[] subclasses = clazz.getDerivedClasses();

		for (int i = 0; i < subclasses.length; i++) {
			if ((subclasses[i].getSuperJavaClass() == clazz)
					|| Arrays.asList(subclasses[i].getImplementedInterfaces())
							.contains(clazz)) {
				if (subclasses[i].getTagByName(tagName) != null) {
					if (!found.contains(subclasses[i])) {
						found.add(subclasses[i]);
					}
				} else {
					getSubclassesWithTagRecursive(found, subclasses[i], tagName);
				}
			}
		}
	}

	public void startComponent(String prefix) {
		componentsPrefixies.push(prefix);
	}

	public void endComponent() {
		componentsPrefixies.pop();
	}

	public String buildComponentColumnName(String columnName) {
		// If columnName is null then we do not need add prefix
		if (columnName == null) {
			return null;
		}

		if (componentsPrefixies.isEmpty()) {
			return columnName;
		}

		StringBuffer result = new StringBuffer();

		for (Iterator iterator = componentsPrefixies.iterator(); iterator
				.hasNext();) {
			String pr = (String) iterator.next();

			if (pr != null) {
				result.append(pr);
			}
		}

		result.append(columnName);
		return result.toString();
	}
	

	protected void populateContextMap(Map map) {
		super.populateContextMap(map);
		map.put("statics", BeansWrapper.getDefaultInstance().getStaticModels());
	}

}
